import React from 'react';
import PropTypes from 'prop-types';
import defaultRequest from './request';
import {
    noop,
    genAppUid,
    genFileUid,
    traverseFileTree,
    validateFileType,
    isPromise,
    datatype
} from './utils';

class Html5Upload extends React.Component {
    static propTypes = {
        /**
         * 自定义[role=application]的样式
         */
        style: PropTypes.object,
        /**
         * 自定义[role=application]的类选择器
         */
        className: PropTypes.string,

        /**
         * input[type=file]的id属性
         */
        id: PropTypes.string,
        /**
         * input[type=file]的name属性
         * 这个字段对应后端接口
         */
        name: PropTypes.string,
        /**
         * 是否可以上传
         * true 为 禁用，不可上传
         * false 为 开启，可上传
         */
        disabled: PropTypes.bool,
        /**
         * 是否支持多选文件
         * `ie10+` 支持
         * true，表示支持，按住 ctrl 可选择多个文件
         * false，表示不支持
         */
        multiple: PropTypes.bool,
        /**
         * 是否支持上传文件夹
         * 仅在 chorme 下生效
         */
        directory: PropTypes.bool,
        /**
         * 接受上传的文件类型 (image/png, image/jpg, .doc, .ppt) 详见 [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept)
         */
        accept: PropTypes.string,
        /**
         * 是否支持拖拽上传
         * `ie10+` 支持
         */
        dragable: PropTypes.bool,

        /////////////

        /**
         * 文件选择回调
         */
        onSelect: PropTypes.func,
        /**
         * 拖拽经过回调
         */
        onDragOver: PropTypes.func,
        /**
         * 拖拽离开回调
         */
        onDragLeave: PropTypes.func,
        /**
         * 拖拽完成回调
         */
        onDrop: PropTypes.func,

        /////////////

        /**
         * 上传的地址
         * 
         * @param {File} file 文件对象
         * @returns {String|Promise<String>}
         */
        action: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
        /**
         * 上传额外传参
         */
        data: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
        /**
         * 设置上传的请求头部
         */
        headers: PropTypes.object,
        /**
         * 是否允许请求携带 cookie
         */
        withCredentials: PropTypes.bool,
        /**
         * 上传文件之前
         * @param {File} file 文件对象
         * @return {Boolean|Promise} `false` 停止上传，非false时表示可继续上传
         */
        beforeUpload: PropTypes.func,
        /**
         * 开始上传时回调
         */
        onStart: PropTypes.func,
        /**
         * 正在上传文件的钩子，参数为上传的事件以及文件
         */
        onProgress: PropTypes.func,
        /**
         * 上传成功回调函数，参数为请求下响应信息以及文件
         */
        onSuccess: PropTypes.func,
        /**
         * 上传失败回调函数，参数为上传失败的信息、响应信息以及文件
         */
        onError: PropTypes.func,
        /**
         * 上传超时,单位ms
         */
        timeout: PropTypes.number,
        /**
         * 上传方法
         */
        method: PropTypes.oneOf(['POST', 'PUT']),
        /**
         * 自定义请求方式
         */
        customRequest: PropTypes.func,
        /**
         * 自定义文件转换
         * 
         * @param {File} file
         * @returns {string | Blob | File | Promise<string | Blob | File>}
         */
        transformFile: PropTypes.func,

        children: PropTypes.node,
    };

    static defaultProps = {
        // style: null,
        // className: undefined,
        // id: undefined,
        name: 'file',
        // disabled: false,
        // multiple: false,
        // directory: false,
        // accept: undefined,
        // dragable: false,
        /////////////
        onSelect: noop,
        onDragOver: noop,
        onDragLeave: noop,
        onDrop: noop,
        /////////////
        // action: undefined,
        // data: null,
        // headers: null,
        // withCredentials: false,
        // beforeUpload: noop,
        onStart: noop,
        onProgress: noop,
        onSuccess: noop,
        onError: noop,
        // timeout: 60 * 1000,
        method: 'POST',
        // customRequest: noop,
        transformFile: f => f
    };

    state = {
        uid: genAppUid(),
        dragState: null
    };
    reqs = {};

    /**
     * 标记是否挂载到DOM上
     * 只有在挂载到DOM之后才能调用post请求上传
     */
    _isMounted = false;
    componentDidMount() {
        this._isMounted = true;
    }
    componentWillUnmount() {
        this._isMounted = false;
        this.abort();
    }

    /**
     * 处理点击上传按钮事件：
     * 用于触发input[type=file]控件的点击效果
     */
    handleOnClick = () => {
        const el = this.fileRef;
        if (!el) {
            return;
        }
        const { children } = this.props;
        if (children && children.type === 'button') {
            el.parentNode.focus();
            el.parentNode.querySelector('button').blur();
        }
        el.click();
    };
    /**
     * 处理键盘事件：Enter键触发
     */
    handleOnKeyDown = e => {
        if (e.key === 'Enter') {
            this.handleOnClick();
        }
    };
    /**
     * 拖拽经过时
     */
    handleOnDragOver = e => {
        e.preventDefault();
        this.setState({
            dragState: 'over'
        });
        this.props.onDragOver(e);
    };
    /**
     * 拖拽离开时
     */
    handleOnDragLeave = e => {
        e.preventDefault();
        this.setState({
            dragState: null
        });
        this.props.onDragLeave(e);
    };
    /**
     * 处理拖拽放开结束事件
     */
    handleOnDrop = e => {
        e.preventDefault();
        this.setState({
            dragState: null
        });

        const {
            directory,
            accept,
            multiple
        } = this.props;
        
        if (directory) {
            traverseFileTree(
                e.dataTransfer.items,
                this.onSelect,
                file => validateFileType(file, accept)
            );
        } else {
            let files = Array.prototype.slice.call(e.dataTransfer.files);

            files = files.filter(file => validateFileType(file, accept));

            if (!multiple) {
                files = files.slice(0, 1);
            }

            this.onSelect(files);
        }
    };

    handleOnChange = e => {
        this.onSelect(e.target.files);
        this.reset();
    };

    /**
     * 当选择了某些文件的时候
     * 它应用于两个场景：
     * 1. 点击从弹窗中选择文件后
     * 2. 拖拽完成之后
     */
    onSelect = targetFiles => {
        const files = Array.prototype.slice.call(targetFiles).map(file => {
            file.uid = genFileUid();
            file.state = 'selected';
            file.percent = 0; // 上传百分比
            file.speed = 0; // 上传速度
            file.loaded = 0; // 已上传多少字节
            file.timeStamp = 0; // 耗时

            return file;
        });

        this.props.onSelect(files);
    };

    startUpload(files) {
        files.forEach(file => {
            this.upload(file);
        });
    }

    upload(file) {
        const {
            beforeUpload,
            onError
        } = this.props;
        if (!beforeUpload) {
            // 异步化发起上传
            return setTimeout(() => {
                this.post(file);
            }, 0);
        }
        const before = beforeUpload(file);
        if (isPromise(before)) {
            before.then(processedFile => {
                const validType = (({
                    File: true,
                    Blob: true
                })[
                    datatype(processedFile)
                ]);

                this.post(
                    validType ?
                    processedFile :
                    file
                );
            }).catch(e => {
                let err = e;
                if (!e) {
                    err = new Error('BEFOREUPLOAD_REJECT');
                    err.code = 'BEFOREUPLOAD_REJECT';
                }
                onError(err, null, file);
            });
        } else if (before !== false) {
            // 异步化发起上传
            setTimeout(() => {
                this.post(file);
            }, 0);
        } else {
            const err = new Error('BEFOREUPLOAD_REJECT');
            err.code = 'BEFOREUPLOAD_REJECT';
            onError(err, null, file);
        }
    }

    post(file) {
        if (!this._isMounted) {
            return;
        }

        const {
            name: filename,
            timeout,
            headers,
            withCredentials,
            method,

            onError
        } = this.props;

        const requestOptions = {
            filename,
            timeout,
            headers,
            withCredentials,
            method
        };

        // 1. action获取，即获取真正的上传地址
        new Promise(resolve => {
            const { action } = this.props;
            if (typeof action === 'function') {
                resolve(action(file));
            } else {
                resolve(action);
            }
        })
        // 收集并缓存上一步获取到的action，当发起上传请求时使用
        .then(action => {
            if (typeof action !== 'string' || !!action === false) {
                const err = new Error('ACTION_ERROR');
                err.code = 'ACTION_ERROR';
                throw err;
            }
            requestOptions.action = action;
        })
        // 2. 文件转换
        .then(() => {
            const { transformFile } = this.props;
            const transformedFile = transformFile(file);

            return transformedFile;
        })
        // 收集并缓存上一步转换后的file，当发起上传请求时使用
        .then(transformedFile => {
            const validType = (({
                String: true,
                File: true,
                Blob: true
            })[
                datatype(transformedFile)
            ]);
            if (!validType || !!transformedFile === false) {
                const err = new Error('TRANSFORMFILE_ERROR');
                err.code = 'TRANSFORMFILE_ERROR';
                throw err;
            }
            requestOptions.file = transformedFile;
        })
        // 3. data处理。然后收集并缓存处理过的data，当发起上传请求时使用
        .then(() => {
            let { data } = this.props;
            if (typeof data === 'function') {
                data = data(file);
            }

            requestOptions.data = data;
        })
        // 4. 发送上传请求
        .then(() => {
            const {
                customRequest,
                onStart,
                onProgress,
                onSuccess
            } = this.props;
            const { uid } = file;

            const request = typeof customRequest === 'function' ? customRequest : defaultRequest;

            this.reqs[uid] = request({
                ...requestOptions,
                onProgress: e => {
                    onProgress(e, file);
                },
                onSuccess: (response, xhr) => {
                    onSuccess(response, xhr, file);
                },
                onError: e => {
                    onError(e, null, file);
                }
            });
            onStart(file);
        })
        .catch(e => {
            let err = e;
            if (!err) {
                err = new Error('POSTUPLOAD_REJECT');
                err.code = 'POSTUPLOAD_REJECT';
            }
            onError(err, null, file);
        });
    }

    /**
     * 终止某个reqs[uid]上传请求
     * @param {string} uid 
     */
    abortRequest(uid) {
        const { reqs } = this;
        if (reqs[uid] && reqs[uid].abort) {
            reqs[uid].abort();
        }
        delete reqs[uid];
    }
    /**
     * 终止某个文件的上传请求
     * @param {File} file 
     */
    abort(file) {
        const { reqs } = this;
        if (file) {
            let uid = file;
            if (file && file.uid) {
                uid = file.uid;
            }
            this.abortRequest(uid);
        } else {
            Object.keys(reqs).forEach(uid => {
                this.abortRequest(uid);
            });
        }
    }

    /**
     * 重置input[type=file]
     */
    reset() {
        this.setState({
            uid: genAppUid()
        });
    }

    saveFileRef = ref => {
        this.fileRef = ref;
    };

    /**
     * 选取data-和aria-前缀的属性，以及role属性
     */
    getDataOrAriaAttributeProps() {
        const { props } = this;

        return Object.keys(props).reduce((acc, key) => {
            const prefix = key.substr(0, 5);
            if (prefix === 'data-' || prefix === 'aria-' || key === 'role') {
                acc[key] = props[key];
            }

            return acc;
        }, {});
    };

    render() {
        const {
            className,
            style,
            id,
            name,
            accept,
            multiple,
            disabled,
            directory,
            draggable,

            children
        } = this.props;

        let events = {};
        if (!disabled) {
            events = {
                onClick: this.handleOnClick,
                onKeyDown: this.handleOnKeyDown,
                tabIndex: '0'
            };
            if (draggable) {
                Object.assign(events, {
                    onDrop: this.handleOnDrop,
                    onDragOver: this.handleOnDragOver,
                    onDragLeave: this.handleOnDragLeave
                });
            }
        }
        return (
            <div
                role="application"
                style={{
                    position: 'relative',
                    zIndex: 0,
                    display: 'inline-block',
                    cursor: 'pointer',
                    ...style
                }}
                className={className}
                {...events}
            >
                <input
                    key={this.state.uid}
                    type="file"
                    id={id}
                    name={name}
                    accept={accept}
                    multiple={multiple}
                    disabled={disabled}
                    directory={directory ? 'directory' : null}
                    webkitdirectory={directory ? 'webkitdirectory' : null}
                    onClick={e => {
                        e.stopPropagation();
                    }}
                    onChange={this.handleOnChange}
                    style={{ display: 'none' }}
                    aria-hidden
                    {...this.getDataOrAriaAttributeProps()}
                    ref={this.saveFileRef}
                />
                {children}
            </div>
        );
    }
}

export default Html5Upload;
